package com.yhn.shortlink.project.service.imp;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yhn.shortlink.common.constant.RedisConstant;
import com.yhn.shortlink.common.entity.LinkAccessStatsDO;
import com.yhn.shortlink.common.entity.LinkDO;
import com.yhn.shortlink.common.entity.LinkGotoDO;
import com.yhn.shortlink.common.enums.LinkErrorEnums;
import com.yhn.shortlink.common.exception.BusinessException;
import com.yhn.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.yhn.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.yhn.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.yhn.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.yhn.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.yhn.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.yhn.shortlink.project.mapper.LinkGotoMapper;
import com.yhn.shortlink.project.mapper.LinkMapper;
import com.yhn.shortlink.project.service.LinkService;
import com.yhn.shortlink.project.utils.HashUtil;
import com.yhn.shortlink.project.utils.LinkUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.yhn.shortlink.project.mapper.LinkAccessStatsMapper;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
* @author 86177
* @description 针对表【link】的数据库操作Service实现
* @createDate 2024-07-27 14:52:10
*/
@Service
@Slf4j
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDO>
    implements LinkService{

    @Resource
    private RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    @Resource
    private LinkGotoMapper linkGotoMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private LinkAccessStatsMapper linkAccessStatsMapper;

    /**
     * 新增短链接
     * @param shortLinkCreateReqDTO 新增请求
     * @return 是否新增成功
     */
    @Override
    public ShortLinkCreateRespDTO addShortLink(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {

        LinkDO shortLinkDO = BeanUtil.toBean(shortLinkCreateReqDTO, LinkDO.class);
        String shortLinkSuffix = generateSuffix(shortLinkCreateReqDTO);
        shortLinkDO.setShortUri(shortLinkSuffix);
        String fullShortUrl = shortLinkCreateReqDTO.getDomain() + "/" + shortLinkSuffix;
        shortLinkDO.setFullShortUrl(fullShortUrl);
        LinkGotoDO linkGotoDO = LinkGotoDO.builder()
                .gid(shortLinkCreateReqDTO.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        try {
            baseMapper.insert(shortLinkDO);
            linkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException ex) {
            //1.短链接存在缓存中
            //2.短链接没有存在缓存中
            LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl, fullShortUrl);
            LinkDO linkDO = baseMapper.selectOne(queryWrapper);
            if(linkDO != null){
                log.warn("短链接:{}生成重复，请重试",fullShortUrl);
                throw new BusinessException(LinkErrorEnums.LINK_GENERATE_REPEAT_ERROR);
            }

        }
        //放入缓存中
        stringRedisTemplate.opsForValue()
                .set(String.format(RedisConstant.GOTO_SHORT_LINK_KEY,fullShortUrl), shortLinkCreateReqDTO.getOriginUrl(), LinkUtil.getLinkCacheValidDate(shortLinkCreateReqDTO.getValidDate()),TimeUnit.MINUTES);
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO
                .builder()
                .originUrl(shortLinkDO.getOriginUrl())
                .fullShortShortUrl(shortLinkDO.getFullShortUrl())
                .gid(shortLinkDO.getGid())
                .build();
    }

    /**
     * 分页查询短链接信息
     * @param shortLinkPageReqDTO 分页查询请求DTO，包含了分页参数和查询条件
     * @return 返回短链接信息的分页数据，每条数据以ShortLinkPageRespDTO形式封装
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO shortLinkPageReqDTO) {
        // 构建查询包装器，设置查询条件
        LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid, shortLinkPageReqDTO.getGid()) // 根据全局唯一标识符（GID）进行查询
                .eq(LinkDO::getDelFlag, 0) // 筛选未被删除的链接（删除标志为0）
                .eq(LinkDO::getEnableStatus, 0); // 筛选禁用状态的链接（启用状态为0）

        // 执行分页查询，获取数据库中的分页结果
        IPage<LinkDO> resultPage = baseMapper.selectPage(shortLinkPageReqDTO, queryWrapper);

        // 将查询结果中的每条数据转换为ShortLinkPageRespDTO对象，然后返回
        return resultPage.convert(
                each -> {
                    ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
                    result.setDomain("http://" + result.getDomain());
                    return result;
                }
        );
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<LinkDO> queryWrapper = Wrappers.query(LinkDO.builder().build())
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .groupBy("gid");
        List<Map<String,Object>> shortLinkCountList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkCountList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean updateShortLink(ShortLinkUpdateReqDTO shortLinkUpdateReqDTO) {
                LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid, shortLinkUpdateReqDTO.getGid())
                .eq(LinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0);
        LinkDO hasLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasLinkDO != null){
            //如果不改变分组
            if(Objects.equals(hasLinkDO.getGid(), shortLinkUpdateReqDTO.getGid())){
                LambdaUpdateWrapper<LinkDO> updateWrapper = Wrappers.lambdaUpdate(LinkDO.class)
                        .eq(LinkDO::getGid, shortLinkUpdateReqDTO.getGid())
                        .eq(LinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                        .eq(LinkDO::getDelFlag, 0)
                        .eq(LinkDO::getEnableStatus, 0)
                        .set(LinkDO::getValidDate, shortLinkUpdateReqDTO.getValidDate());
                LinkDO linkDO = BeanUtil.toBean(hasLinkDO, LinkDO.class);
                baseMapper.update(linkDO, updateWrapper);
            }
            //改变分组
            else{
                LinkDO linkDO = BeanUtil.toBean(hasLinkDO, LinkDO.class);
                LambdaUpdateWrapper<LinkDO> updateWrapper = Wrappers.lambdaUpdate(LinkDO.class)
                        .eq(LinkDO::getGid, hasLinkDO.getGid())
                        .eq(LinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                        .eq(LinkDO::getDelFlag, 0)
                        .eq(LinkDO::getEnableStatus, 0);

                baseMapper.delete(updateWrapper);
                linkDO.setGid(shortLinkUpdateReqDTO.getGid());
                baseMapper.insert(linkDO);
            }
        }
        else{
            throw new BusinessException(LinkErrorEnums.LINK_NOT_EXITS_ERROR);
        }
        return true;
    }

    @Override
    public Boolean redirectLink(String shortUri, HttpServletResponse response, HttpServletRequest request) throws IOException {
        //通过布隆过滤器判断短链接是否存在
        String serverName = request.getServerName();
        String fullShortUrl = serverName + "/" + shortUri;
        //从缓存中获取数据
        String originLink = stringRedisTemplate.opsForValue().get(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        //如果缓存不为空
        if(StrUtil.isNotBlank(originLink)){
            shortLinkStats(fullShortUrl,null,request,response);
            response.sendRedirect(originLink);
            return true;
        }
        //判断布隆过滤器中是否存在
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if(!contains){
            response.sendRedirect("/page/notfound");
            return false;
        }
        //判断是否为空值
        String gotoIsNull = stringRedisTemplate.opsForValue().get(String.format(RedisConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(gotoIsNull)){
            response.sendRedirect("/page/notfound");
            return false;
        }

        //获取分布式锁
        RLock lock = redissonClient.getLock(String.format(RedisConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            //双重判定
            originLink = stringRedisTemplate.opsForValue().get(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originLink)){
                shortLinkStats(fullShortUrl,null,request,response);
                response.sendRedirect(originLink);
                return true;
            }
            else{
                //从跳转表中查询GID
                LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                        .eq(LinkGotoDO::getFullShortUrl, fullShortUrl);
                LinkGotoDO linkGotoDO = linkGotoMapper.selectOne(queryWrapper);
                if(linkGotoDO == null){
                    //此处需要进行封控
                    stringRedisTemplate.opsForValue().set(String.format(RedisConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),"-",30, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return false;
                }
                LambdaQueryWrapper<LinkDO> shortLinkQueryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                        .eq(LinkDO::getGid, linkGotoDO.getGid())
                        .eq(LinkDO::getFullShortUrl, fullShortUrl)
                        .eq(LinkDO::getDelFlag, 0)
                        .eq(LinkDO::getEnableStatus, 0);
                LinkDO linkDO = baseMapper.selectOne(shortLinkQueryWrapper);
                if(linkDO != null){
                    //如果短链接过期
                    if(linkDO.getValidDate()!=null && linkDO.getValidDate().before(new Date())){
                        //加入缓存
                        stringRedisTemplate.opsForValue()
                                .set(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl), "-"
                                        ,LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),TimeUnit.MINUTES);
                        response.sendRedirect("/page/notfound");
                        return false;
                    }
                    //加入缓存
                    stringRedisTemplate.opsForValue()
                            .set(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl), linkDO.getOriginUrl()
                            ,LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),TimeUnit.MINUTES);
                    shortLinkStats(fullShortUrl,linkDO.getGid(),request,response);
                    //进行跳转(重定向)
                    response.sendRedirect(linkDO.getOriginUrl());
                }
            }
        }finally {
            lock.unlock();
        }

        return true;
    }

    /**
     * 根据原始链接生成后缀
     */
    private String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        //可能产生冲突，因此要进行重试
        //自定义生成次数
        int customGenerateTimes = 0;
        String shortUri;
        while (true) {
            if (customGenerateTimes > 10) {
                throw new BusinessException(LinkErrorEnums.LINK_GENERATE_RETRY_ERROR, "短链接生成频繁，请稍后再试");
            }
            String originUrl = shortLinkCreateReqDTO.getOriginUrl();
            originUrl += System.currentTimeMillis();
            shortUri = HashUtil.hashToBase62(originUrl);
            //从布隆过滤器查询是否重复
            if (!shortUriCreateCachePenetrationBloomFilter.contains(shortLinkCreateReqDTO.getDomain() + "/" + shortUri)) {
                break;
            }
            customGenerateTimes++;
        }
        return shortUri;
    }

    /**
     * 短链接计数
     */
    private void shortLinkStats(String fullShortUrl,String gid,HttpServletRequest request, HttpServletResponse response){
        if(StrUtil.isBlank(gid)){
            LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUrl, fullShortUrl);
            LinkGotoDO linkGotoDO = linkGotoMapper.selectOne(queryWrapper);
            gid = linkGotoDO.getGid();
        }

        //获取小时
        int hour = DateUtil.hour(new Date(), true);
        //获取星期
        Week week = DateUtil.dayOfWeekEnum(new Date());
        int weekday = week.getIso8601Value();
        LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                .pv(1)
                .uv(1)
                .uip(1)
                .hour(hour)
                .weekday(weekday)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .gid(gid)
                .build();
        linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
    }
}




